use std::fs::File;
use std::io::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};

pub use shared::*;

use crate::util::shell_escape;
use crate::iptables::{build_iptables_clear_nat, build_iptables_add_root_dnat};
use crate::options::OPTS;

const BASIC_UPLINK_TRACE_NAME: &'static str = "1";

pub fn build_full_command(root_net_ip: &str, user_cmd: &str, tmp_dir: &str, cfgs: Vec<NetemConfig>) -> String {
    let remain_cmd = build_command(root_net_ip, user_cmd, tmp_dir, 0, &cfgs);
    if cfgs.len() > 0 {
        format!("{} && {} && {}", build_iptables_clear_nat(), build_iptables_add_root_dnat(root_net_ip), remain_cmd)
    } else {
        remain_cmd
    }
}

fn build_command(root_net_ip: &str, user_cmd: &str, tmp_dir: &str, depth: u8, cfgs: &[NetemConfig]) -> String {
    if cfgs.is_empty() {
        return user_cmd.to_string();
    }
    let mm_cmd = match cfgs.first().unwrap() {
        NetemConfig::Delay(delay) => build_delay(delay),
        NetemConfig::Jitter(jitter) => build_jitter(tmp_dir, jitter),
        NetemConfig::Loss(loss) => build_loss(loss),
    };
    let remain_cmd = build_command(root_net_ip, user_cmd, tmp_dir, depth + 1, &cfgs[1..]);
    if cfgs.len() > 1 {
        let nat_cmd = build_iptables_inner_dnat(depth + 1);
        format!(
            "{} bash -c \"{} && {}\"",
            mm_cmd,
            nat_cmd,
            shell_escape(&remain_cmd)
        )
    } else {
        format!("{} {}", mm_cmd, remain_cmd)
    }
}

pub fn build_delay(delay: &DelayConfig) -> String {
    match delay {
        DelayConfig::Value(value) => build_delay_stable(*value),
    }
}

pub fn build_loss(loss: &LossConfig) -> String {
    match loss {
        LossConfig::Stable(value) => build_loss_stable(*value),
        LossConfig::Poisson(value) => build_loss_poisson(*value),
    }
}

pub fn build_jitter(tmp_dir: &str, jitter: &JitterConfig) -> String {
    let up_fname = format!("{}/{}.up", tmp_dir, BASIC_UPLINK_TRACE_NAME);
    let _ = generate_no_jitter_trace_file(&up_fname);
    
    match jitter {
        JitterConfig::Stable(value) => {
            let down_fname = format!("{}/jitter_{}.down", tmp_dir, value);
            let _ = generate_jitter_trace_file(*value, &down_fname);
            build_link(&up_fname, &down_fname)
        }
        JitterConfig::Burst { burst, interval } => {
            let down_fname = format!("{}/burst_{}_{}.down", tmp_dir, burst, interval);
            let _ = generate_burst_arrival_trace_file(*burst, *interval, &down_fname);
            build_link(&up_fname, &down_fname)
        }
    }
}

fn build_loss_stable(loss: f32) -> String {
    format!("mm-loss downlink {:.2}", loss)
}

fn build_loss_poisson(loss: f32) -> String {
    format!("mm-onoff downlink {:.2} {:.2}", 1.0 - loss, loss)
}

fn build_delay_stable(rtt: i16) -> String {
    format!("mm-delay {}", rtt / 2)
}

fn build_link(up_fname: &str, down_fname: &str) -> String {
    format!("mm-link {} {} --", up_fname, down_fname)
}

fn generate_no_jitter_trace_file(out_fname: &str) -> anyhow::Result<()> {
    let mut f = File::create(Path::new(out_fname))?;
    const REPEAT: usize = 8;
    for _ in 0..REPEAT {
        writeln!(f, "{}", 1)?;
    }
    Ok(())
}

fn generate_jitter_trace_file(jitter: i16, out_fname: &str) -> anyhow::Result<()> {
    let jitter = jitter.min(1000) as usize;
    let mut f = File::create(Path::new(out_fname))?;
    const N: usize = 1000;
    const REPEAT: usize = 8;
    let mut send_timestamp: Vec<i16> = Vec::with_capacity(N * REPEAT);
    for i in 1..=N {
        for _ in 0..REPEAT {
            send_timestamp.push(i as _);
        }
    }
    /*
    let start_idx = (1000 - jitter) as usize / 2 * REPEAT;
    let new_timestamp = send_timestamp[start_idx] + jitter;
    for i in start_idx..(N * REPEAT) {
        if send_timestamp[i] >= new_timestamp {
            break;
        }
        send_timestamp[i] = new_timestamp;
    }
    */
    let mut start_timestamp: usize = 50;
    while start_timestamp + jitter + 50 < N {
        let new_timestamp = send_timestamp[(start_timestamp + jitter) * REPEAT];
        for i in (start_timestamp * REPEAT) .. (start_timestamp + jitter) * REPEAT {
            send_timestamp[i] = new_timestamp;
        }
        start_timestamp += jitter + 50;
    }
    for i in 0..(N * REPEAT) {
        writeln!(f, "{}", send_timestamp[i])?;
    }
    Ok(())
}

fn generate_burst_arrival_trace_file(
    burst: i16,
    interval: i32,
    out_fname: &str,
) -> anyhow::Result<()> {
    let burst = burst.min(2000);
    let mut f = File::create(Path::new(out_fname))?;
    #[allow(non_snake_case)]
    let N: usize = interval as usize;
    const REPEAT: usize = 8;
    let mut send_timestamp: Vec<i16> = Vec::with_capacity(N * REPEAT);
    for i in 1..=N {
        for _ in 0..REPEAT {
            send_timestamp.push(i as _);
        }
    }
    let start_idx = usize::min(15_000, interval as usize / 2) * REPEAT;
    let new_timestamp = send_timestamp[start_idx] + burst;
    for i in start_idx..(N * REPEAT) {
        if send_timestamp[i] >= new_timestamp {
            break;
        }
        send_timestamp[i] = new_timestamp;
    }
    for i in 0..(N * REPEAT) {
        writeln!(f, "{}", send_timestamp[i])?;
    }
    Ok(())
}

fn build_iptables_inner_dnat(depth: u8) -> String {
    let cur_ip = get_inner_ip(depth);
    let inner_ip = get_inner_ip(depth + 1);

    /*
    // sudo iptables -A PREROUTING -i ingress -t nat -p tcp -d 100.64.0.2 --dport 5080 -j DNAT --to-destination 100.64.0.4
    let tcp_nat = format!("sudo iptables -A PREROUTING -i ingress -t nat -p tcp -d {} --dport 5080 -j DNAT --to-destination {}", cur_ip, inner_ip);

    // sudo iptables -A PREROUTING -i ingress -t nat -p udp -d 100.64.0.2 --dport 9360 -j DNAT --to-destination 100.64.0.4
    let udp_nat = format!("sudo iptables -A PREROUTING -i ingress -t nat -p udp -d {} --dport 9360 -j DNAT --to-destination {}", cur_ip, inner_ip);

    format!("{} && {}", tcp_nat, udp_nat)
    */

    let mut cmds: Vec<String> = Vec::new();
    for tcp_port in OPTS.server_tcp_port.iter() {
        let tcp_cmd = format!(
            "sudo iptables -A PREROUTING -i ingress -t nat -p tcp -d {} --dport {} -j DNAT --to-destination {}",
            cur_ip, tcp_port, inner_ip
            );
        cmds.push(tcp_cmd);
    }
    for udp_port in OPTS.server_udp_port.iter() {
        let udp_cmd = format!(
            "sudo iptables -A PREROUTING -i ingress -t nat -p udp -d {} --dport {} -j DNAT --to-destination {}",
            cur_ip, udp_port, inner_ip,
            );
        cmds.push(udp_cmd);
    }
    cmds.join(" && ")
}

fn get_inner_ip(depth: u8) -> String {
    assert!(depth > 0);
    if depth == 1 {
        String::from("100.64.0.2")
    } else if depth % 2 == 0 {
        String::from("100.64.0.4")
    } else {
        String::from("100.64.0.5")
    }
}
